using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Xml;
using System.Collections.Generic;
using System.Diagnostics;

namespace Dida.Dxml
{
	[AttributeUsage(AttributeTargets.Constructor)]
	public class AutoXmlConstructorAttribute : XmlConstructorAttrBase
	{
		public override Type CreateCompiler(string tagName, Type type, ConstructorInfo ctor)
		{
			AutoXmlConstructorCompilerBuilder builder =
				new AutoXmlConstructorCompilerBuilder(tagName, type, ctor);
			return builder.CompilerType;
		}
	}

	public class ParamDefault : Attribute
	{
		object val;

		public ParamDefault(object value)
		{
			val = value;
		}
	}

	internal class AutoXmlConstructorCompilerBuilder
	{
		Type type;
		ConstructorInfo ctor;

		private class Param
		{
			public ParameterInfo constructorParam;
			public FieldBuilder field;
			public DidaXml.Parser parser;

			public int complexParamIndex;
		}
		Param[] parameters;

		TypeBuilder typeBuilder;
		Type compilerType;
		FieldBuilder referencesSubArrSizesField;
		int nComplexParsers;

		public AutoXmlConstructorCompilerBuilder(string tagName,Type type,ConstructorInfo ctor)
		{
			this.type = type;
			this.ctor = ctor;

			ParameterInfo[] paramsInfo = ctor.GetParameters();
			parameters = new Param[paramsInfo.Length];
			for(int i = 0;i < paramsInfo.Length;i++)
			{
				Param param = new Param();
				parameters[i] = param;
				
				param.constructorParam = paramsInfo[i];
			}

			CreateType(tagName);
			CreateFields();
			CreateLoadMethod();
			CreateGetReferencesMethod();
			CreateCreateObjectMethod();
			CreateObjectTypeProperty();

			compilerType = typeBuilder.CreateType();
		}

		private void CreateType(string tagName)
		{
			typeBuilder = SerializationAssembly.CreateType(tagName + "_XmlLoader");
			typeBuilder.SetParent(typeof(XmlObjectCompilerBase));
		}

		private void CreateFields()
		{
			int nextComplexParamIndex = 0;
			for(int i = 0;i < parameters.Length;i++)
			{
				Param param = parameters[i];
				param.parser = DidaXml.GetParser(
					param.constructorParam.ParameterType);

				switch (param.parser.parserType)
				{
					case DidaXml.ParserType.ComplexParser:
						param.field = typeBuilder.DefineField(param.constructorParam.Name,
							typeof(IComplexParser), FieldAttributes.Private);
						param.complexParamIndex = nextComplexParamIndex;

						nextComplexParamIndex++;
						break;

					case DidaXml.ParserType.Value:
						param.field = typeBuilder.DefineField(param.constructorParam.Name,
							param.constructorParam.ParameterType, FieldAttributes.Private);
						break;

					case DidaXml.ParserType.ExternalFile:
						param.field = typeBuilder.DefineField(param.constructorParam.Name,
							typeof(IExternalStream), FieldAttributes.Private);
						break;
				}
			}

			nComplexParsers = nextComplexParamIndex;
		}

		private void CreateLoadMethod()
		{
			MethodBuilder loadMethod = typeBuilder.DefineMethod(
				"Load", MethodAttributes.Public|MethodAttributes.Virtual,
				typeof(void),new Type[] { typeof(XmlReader),typeof(XmlCompiler) });
			typeBuilder.DefineMethodOverride(loadMethod,
				typeof(XmlObjectCompilerBase).GetMethod("Load"));

			// begin code emitting
			ILGenerator ilGen = loadMethod.GetILGenerator();
			
			ConstructorInfo referenceCtor = typeof(CompilingReference).
				GetConstructor(new Type[] {typeof(string),typeof(XmlPositionMarker)});

			// call the ReadDefAttributes method
			ilGen.Emit(OpCodes.Ldarg_0);		// load the this ptr
			ilGen.Emit(OpCodes.Ldarg_1);		// load the xml reader
			ilGen.Emit(OpCodes.Call, typeof(XmlObjectCompilerBase).GetMethod(
				"ReadDefAttributes", BindingFlags.Instance | BindingFlags.NonPublic));

			foreach (Param param in parameters)
			{
				ilGen.Emit(OpCodes.Ldarg_1);		// load the XmlReader
				ilGen.Emit(OpCodes.Ldstr,param.constructorParam.Name);
				ilGen.Emit(OpCodes.Call, typeof(HelperMethods).GetMethod("GetAttribute"));
				LocalBuilder attrValLocal = ilGen.DeclareLocal(typeof(string));
				ilGen.Emit(OpCodes.Stloc, attrValLocal);

				// parse the string into an object of the required format
				switch (param.parser.parserType)
				{
					case DidaXml.ParserType.ComplexParser:
						ilGen.Emit(OpCodes.Ldarg_0);	// load this ptr for stfld
						ilGen.Emit(OpCodes.Newobj, param.parser.complexParserType.GetConstructor(Type.EmptyTypes));
						ilGen.Emit(OpCodes.Stfld, param.field);
												
						// call the load method:

						ilGen.Emit(OpCodes.Ldarg_0);
						ilGen.Emit(OpCodes.Ldfld, param.field);
						ilGen.Emit(OpCodes.Ldloc, attrValLocal);

						// create the XmlPositionMarker
						ilGen.Emit(OpCodes.Ldarg_1);		// load the XmlReader
						ilGen.Emit(OpCodes.Ldstr, param.constructorParam.Name);
						ilGen.Emit(OpCodes.Call, typeof(XmlPositionMarker).GetMethod("GetAttrPosition",
							new Type[] { typeof(XmlReader), typeof(string) }));

						ilGen.Emit(OpCodes.Callvirt, typeof(IComplexParser).GetMethod("Load"));
						break;

					case DidaXml.ParserType.Value:
						// parse the string value of the attribute, into an 
						// object of the correct type.

						ilGen.BeginExceptionBlock();
						Label endOfExBlock = ilGen.DefineLabel();

						ilGen.Emit(OpCodes.Ldarg_0);	// this ptr for stfld
						
						Debug.Assert(param.parser.parserMethod.IsStatic);
						ilGen.Emit(OpCodes.Ldloc, attrValLocal);
						ilGen.Emit(OpCodes.Call, param.parser.parserMethod);

						// store the parsed value into it's field
						ilGen.Emit(OpCodes.Stfld, param.field);
						ilGen.Emit(OpCodes.Br, endOfExBlock);

						ilGen.BeginCatchBlock(typeof(Exception));

						ilGen.Emit(OpCodes.Ldarg_1);
						ilGen.Emit(OpCodes.Ldstr, param.constructorParam.Name);
						ilGen.Emit(OpCodes.Call, typeof(HelperMethods).GetMethod("ThrowParseAttributeException"));

						ilGen.EndExceptionBlock();
						ilGen.MarkLabel(endOfExBlock);
						break;

					case DidaXml.ParserType.ExternalFile:
						
						// load the parameters for the BeginStreamBlock method

						ilGen.Emit(OpCodes.Ldarg_0);	// load this ptr for stfld

						// acquire the IExternalsLoader
						ilGen.Emit(OpCodes.Ldarg_2);
						ilGen.Emit(OpCodes.Call, typeof(XmlCompiler).GetMethod("GetExternalsLoader"));
						
						ilGen.Emit(OpCodes.Ldloc, attrValLocal);

						// create the xml position marker (the 3rd arg of the BeginStreamMethod)
						ilGen.Emit(OpCodes.Ldarg_1);		// load the xml loader
						ilGen.Emit(OpCodes.Newobj, typeof(XmlPositionMarker).
							GetConstructor(new Type[] { typeof(XmlReader) }));

						ilGen.Emit(OpCodes.Callvirt, typeof(IExternalsLoader).GetMethod("BeginStreamBlock"));
						ilGen.Emit(OpCodes.Stfld, param.field);	// this ptr has been pushed a long time ago

						///////////
						// call the EndStreamBlock method

						// acquire the IExternalsLoader
						ilGen.Emit(OpCodes.Ldarg_2);
						ilGen.Emit(OpCodes.Call, typeof(XmlCompiler).GetMethod("GetExternalsLoader"));
						
						ilGen.Emit(OpCodes.Ldarg_0);
						ilGen.Emit(OpCodes.Ldfld, param.field);

						ilGen.Emit(OpCodes.Callvirt, typeof(IExternalsLoader).GetMethod("EndStreamBlock"));
						break;
				}
			}

			// call the FinalizeNoContentElement method, defined in XmlObjectCompilerBase
			ilGen.Emit(OpCodes.Ldarg_0);		// load the this ptr
			ilGen.Emit(OpCodes.Ldarg_1);		// load the xml reader
			ilGen.Emit(OpCodes.Call, typeof(XmlObjectCompilerBase).GetMethod(
				"FinalizeNoContentElement", BindingFlags.Instance | BindingFlags.NonPublic));

			ilGen.Emit(OpCodes.Ret);
			
			// end of code emitting
		}

		private void CreateGetReferencesMethod()
		{
			MethodBuilder getReferencesMethod = typeBuilder.DefineMethod(
				"GetReferences", MethodAttributes.Public|MethodAttributes.Virtual,
				typeof(CompilingReference[]), Type.EmptyTypes);
			typeBuilder.DefineMethodOverride(getReferencesMethod,
				typeof(XmlObjectCompilerBase).GetMethod("GetReferences"));

			// begin emitting code.

			ILGenerator ilGen = getReferencesMethod.GetILGenerator();

			// create the referencesSubArrSizesField array
			referencesSubArrSizesField = typeBuilder.DefineField(
				"referencesSubArrSizes", typeof(int[]), FieldAttributes.Private);
			ilGen.Emit(OpCodes.Ldarg_0);
			ilGen.Emit(OpCodes.Ldc_I4,nComplexParsers);
			ilGen.Emit(OpCodes.Newarr, typeof(int));
			ilGen.Emit(OpCodes.Stfld, referencesSubArrSizesField);

			LocalBuilder retLocal = ilGen.DeclareLocal(typeof(List<CompilingReference>));
			ilGen.Emit(OpCodes.Newobj, typeof(List<CompilingReference>).GetConstructor(Type.EmptyTypes));
			ilGen.Emit(OpCodes.Stloc, retLocal);

			LocalBuilder sizeLocal = ilGen.DeclareLocal(typeof(int));

			int complexParserIndex = 0;
			
			foreach (Param param in parameters)
			{
				if (param.parser.parserType == DidaXml.ParserType.ComplexParser)
				{
					ilGen.Emit(OpCodes.Ldloc, retLocal);

					ilGen.Emit(OpCodes.Ldarg_0);		// load the this ptr
					ilGen.Emit(OpCodes.Ldfld, param.field);

					ilGen.Emit(OpCodes.Callvirt, typeof(IComplexParser).GetMethod("GetReferences"));

					// save the size of the array return by GetReferences in a local
					ilGen.Emit(OpCodes.Dup);
					ilGen.Emit(OpCodes.Ldlen);
					ilGen.Emit(OpCodes.Stloc, sizeLocal);

					ilGen.Emit(OpCodes.Callvirt, typeof(List<CompilingReference>).GetMethod("AddRange"));
										
					// store the size in the referencesSubArrSizes
					ilGen.Emit(OpCodes.Ldarg_0);
					ilGen.Emit(OpCodes.Ldfld, referencesSubArrSizesField);
					ilGen.Emit(OpCodes.Ldc_I4, complexParserIndex);
					ilGen.Emit(OpCodes.Ldloc, sizeLocal);
					ilGen.Emit(OpCodes.Stelem_I4);
					
					complexParserIndex++;
				}
			}

			ilGen.Emit(OpCodes.Ldloc, retLocal);
			ilGen.Emit(OpCodes.Callvirt, typeof(List<CompilingReference>).GetMethod("ToArray"));
			ilGen.Emit(OpCodes.Ret);

			// end of code emitting
		}

		private void CreateCreateObjectMethod()
		{
			MethodBuilder createObjectMethod = typeBuilder.DefineMethod(
				"CreateObject", MethodAttributes.Public|MethodAttributes.Virtual,
				typeof(object), new Type[] { typeof(XmlCompiler), typeof(object[])});
			typeBuilder.DefineMethodOverride(createObjectMethod,
				typeof(XmlObjectCompilerBase).GetMethod("CreateObject"));

			ILGenerator ilGen = createObjectMethod.GetILGenerator();

			LocalBuilder referencesPtrLocal = ilGen.DeclareLocal(typeof(int));
			int referencesUsingParamIndex = 0;

			// load all params of the constructor onto the stack
			foreach (Param param in parameters)
			{
				switch (param.parser.parserType)
				{
					case DidaXml.ParserType.Value:
						ilGen.Emit(OpCodes.Ldarg_0);	// load the this ptr
						ilGen.Emit(OpCodes.Ldfld, param.field);
						break;

					case DidaXml.ParserType.ComplexParser:
						ilGen.Emit(OpCodes.Ldarg_0);
						ilGen.Emit(OpCodes.Ldfld,param.field);

						ilGen.Emit(OpCodes.Ldarg_2);	// load the references arr
						ilGen.Emit(OpCodes.Ldloc,referencesPtrLocal);
						
						ilGen.Emit(OpCodes.Ldarg_0);
						ilGen.Emit(OpCodes.Ldfld,referencesSubArrSizesField);
						ilGen.Emit(OpCodes.Ldc_I4, referencesUsingParamIndex);
						ilGen.Emit(OpCodes.Ldelem_I4);

						ilGen.Emit(OpCodes.Dup);
						ilGen.Emit(OpCodes.Ldloc,referencesPtrLocal);
						ilGen.Emit(OpCodes.Add);
						ilGen.Emit(OpCodes.Stloc,referencesPtrLocal);

						ilGen.Emit(OpCodes.Call,typeof(ArrayTools).GetMethod("GetSubArray").
							MakeGenericMethod(new Type[] {typeof(object)}));
						
						ilGen.Emit(OpCodes.Callvirt,typeof(IComplexParser).GetMethod("GetObject"));

						// if it's a value type, unbox it
						if (param.parser.targetType.IsValueType)
						{
							ilGen.Emit(OpCodes.Unbox_Any, param.parser.targetType);
						}

						referencesUsingParamIndex++;
						break;
						
					case DidaXml.ParserType.ExternalFile:
						ilGen.Emit(OpCodes.Ldarg_0);
						ilGen.Emit(OpCodes.Ldfld, param.field);
						ilGen.Emit(OpCodes.Callvirt,typeof(IExternalStream).GetMethod("OpenStream"));
						break;
				}				
			}

			// create the object
			ilGen.Emit(OpCodes.Newobj, ctor);

			// leave the ret value on the stack, as the return value of 
			// the CreateObject method.

			ilGen.Emit(OpCodes.Ret);
		}

		private void CreateObjectTypeProperty()
		{
			MethodBuilder getObjTypeMethod = typeBuilder.DefineMethod(
				"get_ObjectType", MethodAttributes.Public|MethodAttributes.Virtual,
				typeof(Type),Type.EmptyTypes);
			typeBuilder.DefineMethodOverride(getObjTypeMethod,
				typeof(XmlObjectCompilerBase).GetProperty("ObjectType").GetGetMethod());

			ILGenerator ilGen = getObjTypeMethod.GetILGenerator();

			ilGen.Emit(OpCodes.Ldtoken, type);
			ilGen.Emit(OpCodes.Call,typeof(Type).GetMethod("GetTypeFromHandle"));
			ilGen.Emit(OpCodes.Ret);
		}

		public Type CompilerType
		{
			get { return compilerType; }
		}		
	}
}